package com.titanrise.crm.common.utils;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.*;

public class Maybe<A> {
    private enum Tag {
        JUST, NOTHING
    }

    private final A value;
    private final Tag tag;

    private Maybe(A value, Tag tag) {
        this.value = value;
        this.tag = tag;
    }

    public boolean isJust() {
        return this.tag == Tag.JUST;
    }

    public boolean isNothing() {
        return this.tag == Tag.NOTHING;
    }

    public Maybe<A> forEach(Consumer<A> consumer) {
        if (this.isJust())
            consumer.accept(value);
        return this;
    }

    public void ifPresent(Consumer<A> consumer) {
        if (this.isJust())
            consumer.accept(value);
    }

    public Maybe<A> filter(Predicate<A> predicate) {
        if (this.isJust() && predicate.test(value))
            return this;
        else
            return Maybe.nothing();
    }

    public <B> Maybe<B> map(Function<A, B> fn) {
        return this.flatMap(fn.andThen(Maybe::just));
    }

    public <B> Maybe<B> ap(Maybe<Function<A, B>> maybeMapper) {
        return maybeMapper.flatMap(this::map);
    }

    public <B> Maybe<B> flatMap(Function<A, Maybe<B>> fn) {
        if (this.isJust())
            return fn.apply(this.value);
        else
            return Maybe.nothing();
    }

    public A orElse(A other) {
        if (this.isJust())
            return this.value;
        else
            return other;
    }

    public A orElseGet(Supplier<? extends A> other) {
        if (this.isJust())
            return this.value;
        else
            return other.get();
    }

    public <X extends Throwable> A orElseThrow(Supplier<? extends X> supplier) throws X {
        if (this.isJust())
            return this.value;
        else
            throw supplier.get();
    }

    public <B> B match(Function<A, B> onJust, Supplier<B> onNothing) {
        if (this.isJust())
            return onJust.apply(this.value);
        else
            return onNothing.get();
    }

    public <B> Maybe<Pair<A, B>> zip(Maybe<B> other) {
        if (this.isJust() && other.isJust())
            return Maybe.just(Pair.of(this.value, other.value));
        else
            return Maybe.nothing();
    }

    public <B, C> Maybe<C> zipWith(BiFunction<A, B, C> fn, Maybe<B> other) {
        if (this.isJust() && other.isJust())
            return Maybe.just(fn.apply(this.value, other.value));
        else
            return Maybe.nothing();
    }

    public static <A> Maybe<A> fromNullable(A a) {
        if (a == null)
            return Maybe.nothing();
        else
            return Maybe.just(a);
    }

    public static <A> Maybe<A> just(A value) {
        return new Maybe<>(value, Tag.JUST);
    }

    public static <A> Maybe<A> nothing() {
        return new Maybe<>(null, Tag.NOTHING);
    }

    public static <L, R> Maybe<R> fromEither(Either<L, R> either) {
        if (either.isLeft())
            return Maybe.nothing();
        else
            return Maybe.just(either.getRight());
    }

    public static <A> Maybe<A> fromOptional(Optional<A> optional) {
        return optional.map(Maybe::just).orElse(Maybe.nothing());
    }

    public static <A> Maybe<List<A>> fromList(List<A> list) {
        return list.isEmpty() ? Maybe.nothing() : Maybe.just(list);
    }

    public Optional<A> toOptional() {
        return Optional.ofNullable(this.value);
    }

    @Override
    public String toString() {
        if (this.isJust())
            return "Maybe.Just(" + this.value + ")";
        else
            return "Maybe.Nothing";
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Maybe<?> maybe = (Maybe<?>) obj;
        return tag == maybe.tag &&
                (Objects.equals(value, maybe.value));
    }
}
